﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
using Microsoft.Scripting;
using Microsoft.Scripting.Hosting;

namespace FishSimulation.Model {
    public class Actor : ModelBase {
        #region Members
        long _BornAtTicks;
        long _LastMatedTicks;        
        double _X, _Y, _Velocity, _Length, _Weight;
        string _ActorTypeId;
        int _Heading;
        double _FoodLevel = 100;
        bool _IsDead = false, _IsMale = false;
        //If another actor is <= this distance it is considered close
        const double NEARBY = 50;
        //If FoodLevelPercent <= this value the actor will try to find food
        const int HUNGER_LEVEL = 70;
        const int REPULSIVE_FIELD = 16;
        const int PARALLEL_FIELD = 32;
        const int ATTACTION_FIED = 50;
        #endregion

        #region Properties
        [Browsable(false)]
        public string ActorTypeId {
            get { return _ActorTypeId; }
            set {
                if (_ActorTypeId != value) {
                    _ActorTypeId = value;
                    this.OnPropertyChanged("ActorTypeId");
                }
            }
        }

        public ActorType ActorType {
            get;
            set;
        }

        public long AgeTicks {
            get {
                return Repository.Instance.World.Ticks - this.BornAtTicks;
            }
        }

        public long BornAtTicks 
        {
            get { return _BornAtTicks; }
            set {
                if (_BornAtTicks != value)
                {
                    _BornAtTicks = value;
                    this.OnPropertyChanged("BornAtTicks");
                }
            }
        }

        private long? DeathTicks 
        {
            get;
            set;
        }

        public bool IsDead {
            get {
                return _IsDead;
            }
            set {
                if (_IsDead != value) {
                    _IsDead = value;
                    this.OnPropertyChanged("IsDead");
                }
            }
        }

        public bool IsMale {
            get { return _IsMale; }
            set {
                if (_IsMale != value) {
                    _IsMale = value;
                    this.OnPropertyChanged("IsMale");
                }
            }
        }

        /// <summary>
        /// 0 = dead fish
        /// 100 = full fish
        /// </summary>
        public double FoodLevel {
            get {
                return _FoodLevel;
            }
            set {
                if (value > this.ActorType.MaxFoodLevel)
                    value = this.ActorType.MaxFoodLevel;
                if (_FoodLevel != value) {
                    _FoodLevel = value;
                    this.OnPropertyChanged("FoodLevel");
                }
            }
        }

        public int Heading {
            get {
                return _Heading;
            }
            set {
                if (_Heading != value) {
                    _Heading = value % 360;
                    this.OnPropertyChanged("Heading");
                }
            }
        }

        public long LastMatedTicks 
        {
            get
            {
                return _LastMatedTicks;
            }
            set
            {
                if (_LastMatedTicks != value)
                {
                    _LastMatedTicks = value;
                    this.OnPropertyChanged("LastMatedTicks");
                }
            }
        }

        public double Length {
            get { return _Length; }
            set {
                if (_Length != value) {
                    _Length = value;
                    this.OnPropertyChanged("Length");
                }
            }
        }

        public double Velocity {
            get {
                return _Velocity;
            }
            set {
                if (_Velocity != value) {
                    _Velocity = value;
                    this.OnPropertyChanged("Velocity");
                }
            }
        }

        public double Weight {
            get {
                return _Weight;
            }
            private set {
                if (_Weight != value) {
                    _Weight = value;
                    this.OnPropertyChanged("Weight");
                }
            }
        }

        public double X {
            get { return _X; }
            set {
                if (_X != value) {
                    _X = value;
                    this.OnPropertyChanged("X");
                }
            }
        }

        public double Y {
            get { return _Y; }
            set {
                if (_Y != value) {
                    _Y = value;
                    this.OnPropertyChanged("Y");
                }
            }
        }
        #endregion

        #region Events
        public event ActionEventHandler Died;
        public event MatedEventHandler Mated;
        public delegate void ActionEventHandler(Actor actor);
        public delegate void MatedEventHandler(Actor actor, MatedEventArgs e);
        public void OnDied(Actor actor) {
            if (this.Died != null) {
                actor.IsDead = true;
                this.Died(actor);
            }
        }
        public void OnMated(Actor actor, MatedEventArgs e){
            if (this.Mated != null) {
                this.Mated(actor, e);
            }
        }
        public class MatedEventArgs : EventArgs {
            public List<Actor> OffSpring {
                get;
                set;
            }
        }
        #endregion

        #region Methods
        public static Actor Create(ActorType type) {
            if (type == null)
                throw new Exception("Null type not supported");
            if (type.Id == null)
                throw new Exception("Actor Type ID not set");

            Actor f = new Actor();            
            f.ActorType = type;
            f.ActorTypeId = type.Id;            
            long rand = Utils.RandomInt64(0, Repository.Instance.World.Ticks);
            f.BornAtTicks = rand;
            f.FoodLevel = Utils.RandomInt(75, 100);
            f.Heading = Utils.RandomInt(0, 360);
            f.IsDead = false;
            f.IsMale = Utils.RandomBoolean();            
            f.Length = Utils.RandomInt(type.MinLengthInches, type.MaxLengthInches);
            f.Velocity = Utils.RandomInt(0, type.MaxVelocity);
            f.Weight = 0;
            var cord = Repository.Instance.World.Pond.GetRandomCord();
            f.X = cord.X;
            f.Y = cord.Y;
            
            return f;
        }

        public override bool Equals(object obj) {
            return base.Equals(obj);
        }

        /// <summary>
        /// Gets the closest actor to this actor from the given list
        /// </summary>
        /// <param name="list"></param>
        /// <param name="dist">The distance to the predator</param>
        /// <returns></returns>
        public Actor GetClosest(List<Actor> list, out double dist) {
            Actor closest = null;
            dist = Double.MaxValue;

            if (list != null && list.Count > 0) {                
                closest = list[0];
                foreach (Actor a in list) {
                    var d = Utils.Distance(a.X, a.Y, this.X, this.Y);
                    if (d < dist) {
                        dist = d;
                        closest = a;
                    }
                }
            }

            return closest;
        }

        public void Eat(Actor food) {            
            //Actor.Name will refer to the actor doing the eating!!!
            //Other.Actor.Name will refer to the actor being eaten!!!
            this.FoodLevel += this.ProcessFormula(this.ActorType.FoodValueFormula, food);            
            this.OnDied(food);         
        }

        private List<Actor> Mate(Actor mate) {
            List<Actor> offspring = new List<Actor>();
            int n = Utils.RandomInt(0, this.ActorType.MaxNumberOfOffSpring);
            for (int i = 0; i < n; i++) {
                var o = Create(this.ActorType);                
                o.BornAtTicks = 0;
                o.FoodLevel = 100;               
                o.Length = this.ActorType.MinLengthInches;
                o.Velocity = 1;
                o.Weight = 0;
                o.X = this.X + Utils.RandomInt(-7, 7);
                o.Y = this.Y + Utils.RandomInt(-7, 7);
                //set this so actors don't mate out of control
                o.LastMatedTicks = Repository.Instance.World.Ticks;
                offspring.Add(o);
            }
            mate.LastMatedTicks = Repository.Instance.World.Ticks;
            this.LastMatedTicks = Repository.Instance.World.Ticks;            
            return offspring;
        }

        private void Metabolize() {
            //Actor.Name will refer to the actor doing the metabolizing!!!            
            this.FoodLevel -= this.ProcessFormula(this.ActorType.FoodDecrementFormula, null);
            this.Weight = this.ProcessFormula(this.ActorType.WeightFormula, null);
        }

        /// <summary>
        /// Executes one move in the game for the actor.
        /// </summary>
        public void Move() {
            if (this.ActorType == null) return;

            double x = this.X;
            double y = this.Y;

            bool eater = this.ActorType.Menu != null && this.ActorType.Menu.Count > 0;
            
            //shrimp, grass, etc.. do not need to eat
            if(eater){
                this.Metabolize();
            }

            //if the fish is out of food it is dead!
            if (this.FoodLevel <= 0) {
                this.OnDied(this);
                return;
            }

            //if the creature has outlived his life span it is dead
            if (this.ActorType.LifeSpanMaxTicks > 0) {
                //has the creature lived past the min ticks?
                long age = Repository.Instance.World.Ticks - this.BornAtTicks;
                if (age > this.ActorType.LifeSpanMinTicks) {
                    if (this.DeathTicks == null) {
                        this.DeathTicks = Utils.RandomInt64(this.ActorType.LifeSpanMinTicks, this.ActorType.LifeSpanMaxTicks);
                    }
                    if (age > this.DeathTicks) {
                        this.OnDied(this);   
                    }
                }
            }

            //get all nearby actors
            var near = Repository.Instance.World.GetByBounds(this.X - NEARBY, this.Y - NEARBY, NEARBY * 2, NEARBY * 2);
            near.Remove(this);

            //actors that can eat the current object type
            var predators = near.Where(xx => xx.ActorType.Menu.Contains(this.ActorType) && xx != this).ToList();

            //avoid being eaten!
            if (predators.Count > 0) { 
                //get closest predator
                var dist = 0.0;
                var np = this.GetClosest(predators, out dist);
                //turn away from predator
                this.Turn(Utils.RandomInt(90, 270));
                //swim as fast as the fish can
                this.Velocity = this.ActorType.MaxVelocity;
                x += this.Velocity * (Math.Cos(this.Heading * Math.PI / 180));
                y += this.Velocity * (Math.Sin(this.Heading * Math.PI / 180));
                this.TryXY(x, y);
                return;
            }

            //if hungry, the actor needs to find food
            if (this.FoodLevel <= HUNGER_LEVEL) {
                //look for nearby items to eat
                var dinner = near.Where(xx => this.ActorType.Menu.Contains(xx.ActorType)).ToList();
                if (dinner != null && dinner.Count > 0) {                                        
                    var dist2 = 0.0;
                    var nd = this.GetClosest(dinner, out dist2);
                    if (dist2 <= this.Length) {
                        this.Eat(nd);                        
                        return;
                    }
                    else {
                        //set max velocity to catch dinner
                        this.Velocity = this.ActorType.MaxVelocity;
                        var diffy = this.Y - nd.Y;
                        var diffx = this.X - nd.X;
                        var angle = Math.Atan(diffy / diffx) * 180 / Math.PI;
                        x += this.Velocity * (Math.Cos(angle * Math.PI / 180));
                        y += this.Velocity * (Math.Sin(angle * Math.PI / 180));
                        this.TryXY(x, y);
                        return;
                    }
                }
            }

            //actors that are the same type as current object
            var same = near.Where(xx => xx.ActorType == this.ActorType && xx != this).ToList();

            //if not being chased, and not trying to catch food, attempt to mate
            if (same.Count > 0 && this.ActorType.CanMate && Repository.Instance.World.Ticks - this.LastMatedTicks >= this.ActorType.WaitToMateTicks)
            {
                var oppsex = near.Where(xx => xx.IsMale != this.IsMale).ToList();
                if (oppsex != null && oppsex.Count > 0)
                {
                    var dist = 0.0;
                    var mate = this.GetClosest(oppsex, out dist);
                    if (dist <= this.Length)
                    {
                        //mate
                        this.OnMated(this, new MatedEventArgs() { OffSpring = this.Mate(mate) });
                        return;
                    }
                    else
                    {
                        this.Velocity = Utils.RandomInt(1, this.ActorType.MaxVelocity);
                        var diffy = this.Y - mate.Y;
                        var diffx = this.X - mate.X;
                        var angle = Math.Atan(diffy / diffx) * 180 / Math.PI;
                        x += this.Velocity * (Math.Cos(angle * Math.PI / 180));
                        y += this.Velocity * (Math.Sin(angle * Math.PI / 180));
                        this.TryXY(x, y);
                        return;
                    }
                }
            }

            //no predators around, relax
            this.Velocity = Utils.RandomInt(1, this.ActorType.MaxVelocity / 2);

            if (this.ActorType.Schools)
            {
                //http://www.complexity.org.au/ci/vol08/inada01/inada01.pdf
                if (same != null && same.Count > 0) {
                    var dist = 0.0;
                    var closest = this.GetClosest(same, out dist);
                    bool schooling = false;
                    if (dist <= REPULSIVE_FIELD) {
                        this.Heading += Utils.RandomInt(0, 360);
                        schooling = true;
                    }
                    else if (dist <= PARALLEL_FIELD) {
                        this.Heading = closest.Heading;
                        schooling = true;
                    }
                    else if (dist <= ATTACTION_FIED) {                        
                        schooling = true;
                        var diffy = this.Y - closest.Y;
                        var diffx = this.X - closest.X;
                        var angle = Math.Atan(diffy / diffx) * 180 / Math.PI;
                        x += this.Velocity * (Math.Cos(angle * Math.PI / 180));
                        y += this.Velocity * (Math.Sin(angle * Math.PI / 180));
                    }

                    if (schooling)
                    {
                        //set new X, Y coordinate
                        x += this.Velocity * (Math.Cos(this.Heading * Math.PI / 180));
                        y += this.Velocity * (Math.Sin(this.Heading * Math.PI / 180));
                        this.TryXY(x, y);
                    }
                }
            }

            //set new X, Y coordinate
            x += this.Velocity * (Math.Cos(this.Heading * Math.PI / 180));
            y += this.Velocity * (Math.Sin(this.Heading * Math.PI / 180));
            this.TryXY(x, y);
        }

        private double ProcessFormula(string formula, Actor other) {
            if (formula == null) return 0;

            //if formula is a constant like 10, just return the value
            double? dx = formula.TryParseDouble();
            if (dx != null) return dx.Value;

            //pick out arguments
            Regex rex = new Regex(@"\{(Other\.)*(ActorType|Actor|World|Pond)\..+?\}", RegexOptions.IgnoreCase);
            MatchCollection matches = rex.Matches(formula);
            Hashtable varmap = new Hashtable();
            foreach (Match m in matches) {
                if (!varmap.Contains(m.Value)) {
                    string exp = m.Value.Replace("{", String.Empty).Replace("}", String.Empty);
                    string[] parts = exp.Split('.');
                    bool isother = parts[0] == "Other";
                    string @class = !isother ? parts[0] : parts[1];
                    string prop = !isother ? parts[1] : parts[2];

                    object value = null;
                    object target = null;
                    if (@class == "ActorType") {
                        target = !isother ? this.ActorType : other.ActorType;
                    }
                    else if (@class == "Actor") {
                        target = !isother ? this : other;
                    }
                    else if (@class == "World") {
                        target = Repository.Instance.World;
                    }
                    else if (@class == "Pond") {
                        target = Repository.Instance.World.Pond;
                    }
                    else throw new Exception("Unknown expression: " + m.Value);

                    if (target != null) {
                        PropertyInfo propInfo = Cache.Get(m.Value) as PropertyInfo;
                        if (propInfo == null) {
                            propInfo = target.GetType().GetProperty(prop);
                        }
                        if (propInfo != null) {
                            Cache.Set(m.Value, propInfo);
                            value = propInfo.GetValue(target, null);
                        }
                    }

                    varmap.Add(m.Value, value);
                }
            }

            string script = formula;
            foreach (var key in varmap.Keys) {
                object v = varmap[key];
                string cleanv = (v ?? "").ToString();
                if (v is string) {
                    cleanv = @"""{0}""".Formatt(cleanv);
                }
                string varname = key.ToString().Replace("{", String.Empty).Replace("}", String.Empty).Replace(".", "_");
                script = "{0} = {1}{2}".Formatt(varname, cleanv, Environment.NewLine) + script;
                script = script.Replace(key.ToString(), varname);
            }

            //execute the script and get the value back
            ScriptSource source = Utils.pyEngine.CreateScriptSourceFromString(script, SourceCodeKind.Statements);
            CompiledCode compiled = source.Compile();
            // Executes in the scope of Python
            compiled.Execute(Utils.pyScope);
            var result = Utils.pyScope.GetVariable("result");
            
            return result;
        }

        private int? turning = null;
        private void TryXY(double x, double y) {
            if (Repository.Instance.World.Pond.InPond(new Cord(x, y)))
            {
                turning = null;
                this.X = x;
                this.Y = y;
            }
            else {
                if (turning == null)
                {
                    //turn 
                    if (this.Heading <= 45)
                    {
                        turning = 10;
                    }
                    else if (this.Heading <= 90)
                    {
                        turning = -10;
                    }
                    else if (this.Heading <= 135)
                    {
                        turning = 10;
                    }
                    else if (this.Heading <= 180)
                    {
                        turning = -10;
                    }
                    else if (this.Heading <= 225)
                    {
                        turning = 10;
                    }
                    else if (this.Heading <= 270)
                    {
                        turning = -10;
                    }
                    else if (this.Heading <= 315)
                    {
                        turning = 10;
                    }
                    else if (this.Heading <= 360)
                    {
                        turning = -10;
                    }
                }
                this.Heading += turning.Value;
            }
        }

        public void Turn(int degrees) {
            this.Heading += degrees;
            this.Heading = this.Heading % 360;
        }

        public override string ToString() {
            string s = null;
            if (this.ActorType != null)
                s = this.ActorType.ToString();
            else
                s = base.ToString();
            return s;
        }
        #endregion
    }
}
